Ordinary
About

Java Object Class

profileordilov / 2022. 4. 30

Object Class

Java의 Object Class를 구현 부분을 간단하게 살펴보겠습니다.

@IntrinsicCandidate

Object 구현 코드를 살펴보면 내부 구현 없이 @IntrinsicCandidate 어노테이션이 명시된 부분이 많습니다. 이 어노테이션은 자바에서 구현이 JVM 상에서 해주는 부분을 명시합니다. Intrinsic 뜻 자체가 '고유' 라는 뜻을 가지다 보니 기본적으로 제공되는 기능으로 생각해도 됩니다. 이렇게 명시된 코드들의 특징은 구현 부분이 컴파일러나 인터프리터에 의해 변경될 수 있습니다. 이렇게 제공되었을 때 장점은 2가지입니다.

  • 하드웨어와 가까운 더 낮은 레벨의 코드로 구현될 수 있다.
  • 그대로 사용하지만 버전에 따라 구현을 변경할 수 있다.

더 낮은 레벨의 코드를 사용하는 경우, 성능이 더 뛰어나거나, 자바 코드 상에서는 불가능한 구현들을 가능하게 합니다.

생성자

@IntrinsicCandidate public Object() {}

생성자도 마찬가지로 JVM 내부에서 구현되어 있습니다.

getClass

@IntrinsicCandidate public final native Class<?> getClass();

말 그대로 클래스 정보를 가져오는데 주의할 점은 선언한 변수 클래스가 아니라 생성한 클래스 정보를 가져옵니다.

Animal animal = new Dog(); Class animalClass = animal.getClass();

예를 들어 Animal을 상속받은 Dog 클래스가 있다고 할 때, Animal 클래스로 선언한 변수에서 getClass()를 하더라도 Dog 클래스 정보를 불러옵니다. 그 결과 실제 구현 클래스 정보를 불러오고 싶을 때 따로 캐스팅이 필요 없습니다.

native 키워드는 JNI(Java Native Interface)로 구현된 것을 나타내는 키워드로 운영체제와 더 가까운 c나 c++ 같은 언어로 구현된 코드와 매칭됩니다.

hashCode

@IntrinsicCandidate public native int hashCode();

기본 해시코드도 @IntrinsicCandiate로 구현되어 있습니다. 따라서 JVM 종류에 따라 구현이 달라지게 되고 native 키워드도 있기 때문에 자바 언어로 구현되어 있지 않다는 걸 알 수 있습니다. OpenJDK 8 기준으로 어떤 식으로 구현되는지 간단하게 살펴보겠습니다.

  1. OS 랜덤 값
  2. 오브젝트 메모리 주소 변환 값
  3. 1
  4. 증가하는 연속된 값
  5. 오브젝트 메모리 주소를 int 로 캐스팅
  6. xorshift 랜덤 값

각 방법마다 장단점이 있지만 기본적으로 사용하는 방법은 5번입니다. 이는 다음에 나올 equals() 의 구현과도 연관이 있는데 Object의 equals()는 참조값을 기준으로 같은 객체인지 비교합니다. 따라서 hashCode() 도 기본 구현은 참조값을 기준으로 생성합니다.

hashCode()는 2가지 규칙을 지켜야 합니다.

  • 내부 값을 변경하지 않는한 실행 중에 항상 일관된 값을 반환합니다.
  • equals()가 참인 경우 같은 값을 반환합니다.

hashCode()가 같더라도 equals()가 참이 아닐 수는 있지만 해시 테이블 성능을 생각했을 때 최대한 다른 객체면 다른 해시코드를 반환하는 것이 좋습니다.

equals

public boolean equals(Object obj) { return (this == obj); }

hashCode()를 설명하면서 간단하게 나왔지만 기본은 참조값을 비교합니다. equals() 는 null이 아닐 때 5가지 규칙을 지켜야 합니다.

  • 자기 자신을 비교할 때 참을 반환합니다.
  • 대칭적으로 a.equals(b) 라면 b.equals(a)도 참이여야 합니다.
  • 전파적으로 (a,b), (b,c) 가 같다면 (a, c)도 같아야 합니다.
  • 여러번 반복해도 일관된 결과를 반환해야 합니다.
  • null과 비교할 때 거짓을 반환해야 합니다.

Override하는 경우 앞서 설명한 hashCode와 함께 override를 해야합니다.

clone

@IntrinsicCandidate protected native Object clone() throws CloneNotSupportedException;

clone 은 객체에 대한 얕은 복사를 하는 함수입니다. 깊은 복사가 필요하고 내부에 참조하는 다른 객체가 있다면 override해서 참조하는 객체도 새로 생성해줄 필요가 있습니다.

clone의 반환값은 3가지 규칙을 만족해야 합니다.

  • 원본 참조값 == clone 참조값
  • 원본.getClass() == clone.getClass()
  • 원본.equals() == clone.equals()

toString

public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }

문자열로 출력할 때 사용되며 기본은 클래스 이름과 참조값이 출력됩니다.

finalize

@Deprecated(since="9") protected void finalize() throws Throwable { }

finalize는 객체가 더 이상 사용되지 않는 것을 직접 명시해서 가비지컬렉터를 부르게 되는데 deprecated 되었듯이 성능상의 이슈가 있을 수 있어 더 이상 사용되지 않습니다.

notify

@IntrinsicCandidate public final native void notify();

객체가 모니터 중이고 대기 중인 임의의 쓰레드 하나를 사용가능하게 합니다. 다만 notify()를 한다고 바로 다른 쓰레드가 실행되는게 아니라 notify()를 호출한 메소드가 완료되거나 wait()처럼 lock을 포기할 때 사용가능합니다. 객체가 소유중인지 모니터 중이려면 세 가지 방법중 하나를 택해야 합니다.

  • synchronized 메소드를 실행할 때
  • synchronzied static 메소드를 실행할 때
  • synchronized body 선언 부분일 때

notifyAll

@IntrinsicCandidate public final native void notifyAll();

notifyAll()notify()와 마찬가지로 대기중인 다른 쓰레드를 사용 가능하게 하는데 차이점은 임의의 쓰레드 하나가 아닌 전부 사용 가능하게 됩니다.

wait

public final native void wait(long timeoutMillis) throws InterruptedException;

쓰레드를 대기중인 상태로 변경하는 메소드입니다. 대기중에서 사용가능한 상태로 바뀌려면 notify 되거나, interrupt 되거나 지정한 timeout 시간이 지나야 합니다.

notify와 wait

다른 부분들은 모든 객체에서 사용하는 부분이지만 notify와 wait은 멀티 쓰레드 작업을 할 때만 사용됩니다. Object 클래스가 아닌 Thread 클래스에 있어도 될 것 같지만 Object 클래스에 위치한 이유가 있습니다.

자바에서 쓰레드가 어떤 객체에 접근해서 작업하려고 할 때 사용중인지 모니터링 하는 역할을 객체가 담당합니다. 객체에 접근 중인 쓰레드가 더 이상 없어 사용 가능한 상태인 걸 판단하는 것도 객체에서 하게 됩니다. 따라서 쓰레드를 대기하거나 사용 가능하게 하는 메소드가 Object 클래스에 위치하게 됩니다.